// Package daemon 守护进程模式
package daemon

import (
    "context"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "log/slog"
    "os"
    "path/filepath"
    "runtime/debug"
    "strconv"
    "strings"
    "sync"
    "time"

    "github.com/cnssl/cert-deploy/internal/backup"
    "github.com/cnssl/cert-deploy/internal/config"
    "github.com/cnssl/cert-deploy/internal/csr"
    "github.com/cnssl/cert-deploy/internal/deployer"
    "github.com/cnssl/cert-deploy/internal/fetcher"
    "github.com/cnssl/cert-deploy/internal/validator"
    "github.com/cnssl/cert-deploy/internal/util"
)

// Daemon 守护进程
type Daemon struct {
	cfgManager *config.Manager
	logger     *slog.Logger
	stopCh     chan struct{}
}

// New 创建守护进程
func New(cfgManager *config.Manager, logger *slog.Logger) *Daemon {
	return &Daemon{
		cfgManager: cfgManager,
		logger:     logger,
		stopCh:     make(chan struct{}),
	}
}

// Run 运行守护进程
func (d *Daemon) Run() error {
    d.logger.Info("daemon started")

    ticker := time.NewTicker(10 * time.Minute) // 基础检查间隔
    defer ticker.Stop()

    // 使用 context 传递取消信号，实现优雅退出
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 监听 stopCh
    go func() {
        <-d.stopCh
        d.logger.Info("stop signal received, shutting down...")
        cancel()
    }()

    // 立即执行一次检查
    if err := d.checkAndDeployWithContext(ctx); err != nil && err != context.Canceled {
        d.logger.Error("initial check failed", "error", err)
    }

    for {
        select {
        case <-ticker.C:
            if err := d.checkAndDeployWithContext(ctx); err != nil {
                if err == context.Canceled {
                    d.logger.Info("daemon stopped")
                    return nil
                }
                d.logger.Error("check failed", "error", err)
            }
        case <-ctx.Done():
            d.logger.Info("daemon stopped")
            return nil
        }
    }
}

// Stop 停止守护进程
func (d *Daemon) Stop() {
	close(d.stopCh)
}

// checkAndDeploy 检查并部署证书
// checkAndDeployWithContext 并发检查与部署，支持取消
func (d *Daemon) checkAndDeployWithContext(ctx context.Context) error {
    d.logger.Info("checking sites")

    sites, err := d.cfgManager.ListSites()
    if err != nil {
        return fmt.Errorf("failed to list sites: %w", err)
    }

    // 读取并发度（默认 5，可通过环境变量 MAX_WORKERS 调整）
    maxWorkers := 5
    if v := os.Getenv("MAX_WORKERS"); v != "" {
        if n, err := strconv.Atoi(v); err == nil && n > 0 {
            maxWorkers = n
        }
    }

    var wg sync.WaitGroup
    sem := make(chan struct{}, maxWorkers)

    for _, site := range sites {
        s := site
        if !s.Enabled || !d.shouldProcess(s) {
            continue
        }
        wg.Add(1)
        go func() {
            defer wg.Done()
            select {
            case sem <- struct{}{}:
                defer func() { <-sem }()
                if err := d.processSiteWithContext(ctx, s); err != nil {
                    if err != context.Canceled {
                        d.logger.Error("failed to process site", "site", s.SiteName, "error", err)
                    }
                }
            case <-ctx.Done():
                return
            }
        }()
    }

    wg.Wait()
    return ctx.Err()
}

// shouldProcess 判断是否应该处理该站点
func (d *Daemon) shouldProcess(site *config.SiteConfig) bool {
	now := time.Now()

	// 1. 检查是否到了重试时间
	if !site.Metadata.NextRetryAt.IsZero() && now.After(site.Metadata.NextRetryAt) {
		return true
	}

	// 2. 检查是否需要续期
	if site.NeedsRenewal() {
		// 检查上次检查时间,避免频繁检查
		if site.Metadata.LastCheckAt.IsZero() ||
			now.Sub(site.Metadata.LastCheckAt) > time.Duration(site.Schedule.CheckIntervalHours)*time.Hour {
			return true
		}
	}

	return false
}

// processSite 处理单个站点
func (d *Daemon) processSiteWithContext(ctx context.Context, site *config.SiteConfig) (err error) {
    // Panic 恢复，避免单站点异常导致守护进程崩溃
    defer func() {
        if r := recover(); r != nil {
            stack := debug.Stack()
            d.logger.Error("recovered from panic", "site", site.SiteName, "panic", r, "stack", string(stack))
            err = fmt.Errorf("panic: %v", r)
        }
    }()

    // 为单个站点处理设置超时（30s）
    cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    f := fetcher.New(30 * time.Second)
    d.logger.Info("daemon GET info", "site", site.SiteName, "refer", util.MaskReferID(site.API.ReferID))
    info, err := f.Info(cctx, site.API.URL, site.API.ReferID)
    if err != nil {
        d.logger.Error("failed to GET cert info", "site", site.SiteName, "error", err)
        site.Metadata.LastCheckAt = time.Now()
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }

    // 若 file 验证，尝试写入挑战文件
    if info.File != nil && strings.EqualFold(site.Validation.Method, "file") {
        if site.Paths.Webroot != "" {
            // 安全拼接，限制到 .well-known 下且不允许逃逸
            target, jerr := safeJoinWebroot(site.Paths.Webroot, info.File.Path)
            if jerr != nil {
                d.logger.Warn("invalid file challenge path", "site", site.SiteName, "error", jerr)
            } else {
                if err := os.MkdirAll(filepath.Dir(target), 0755); err == nil {
                    if writeErr := os.WriteFile(target, []byte(info.File.Content), 0644); writeErr != nil {
                        d.logger.Warn("write file challenge failed", "site", site.SiteName, "error", writeErr)
                    } else {
                        d.logger.Info("file challenge written", "path", target)
                    }
                }
            }
        } else {
            d.logger.Warn("webroot not set for file validation", "site", site.SiteName)
        }
    }

    site.Metadata.LastCheckAt = time.Now()

    // 若服务端已是 active，且新证书更“新”，直接部署
    if strings.EqualFold(info.Status, "active") && info.Cert != "" {
        if err := d.tryDeployIfNew(site, info); err != nil {
            return err
        }
        // 无需继续 POST
        return nil
    }

    // 判定是否需要发起更新（POST），控制重试次数
    if site.Metadata.IssueRetryCount >= 3 && site.Metadata.LastIssueState == "unpaid" {
        d.logger.Warn("unpaid retry limit reached", "site", site.SiteName)
        return d.cfgManager.SaveSite(site)
    }

    // 生成 CSR 并提交（当状态非 active 或证书未更新）
    commonName := info.CommonName
    if commonName == "" {
        // 兜底使用站点域名
        if len(site.Domains) > 0 { commonName = site.Domains[0] } else { commonName = site.SiteName }
    }

    // 生成 CSR/Key（仅在发起更新时执行）
    keyPEM, csrPEM, csrHash, err := d.generateCSR(commonName, site.Key, site.CSR)
    if err != nil {
        d.logger.Error("generate CSR failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    // 覆盖写入私钥文件（0600）
    if err := os.MkdirAll(filepath.Dir(site.Paths.PrivateKey), 0700); err == nil {
        _ = os.WriteFile(site.Paths.PrivateKey, []byte(keyPEM), 0600)
    }

    d.logger.Info("daemon POST start", "site", site.SiteName, "refer", util.MaskReferID(site.API.ReferID), "method", site.Validation.Method)
    post, err := f.StartOrUpdate(cctx, site.API.URL, site.API.ReferID, csrPEM, commonName, site.Validation.Method)
    if err != nil {
        d.logger.Error("POST update failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }

    site.Metadata.LastCSRHash = csrHash
    site.Metadata.CSRSubmittedAt = time.Now()
    site.Metadata.LastIssueState = post.Status

    switch strings.ToLower(post.Status) {
    case "unpaid":
        site.Metadata.IssueRetryCount++
        site.Metadata.NextRetryAt = time.Now().Add(6 * time.Hour)
        d.logger.Info("order unpaid, will retry", "retry_count", site.Metadata.IssueRetryCount)
    case "pending", "processing", "approving":
        site.Metadata.NextRetryAt = time.Now().Add(30 * time.Minute)
        d.logger.Info("awaiting issuance", "state", post.Status)
    case "active":
        // 直接 active：若新证书更“新”则部署，否则一天后重试（不超过3次）
        if err := d.tryDeployIfNew(site, post); err != nil {
            return err
        }
        // 若未部署（不够新）
        site.Metadata.IssueRetryCount++
        if site.Metadata.IssueRetryCount <= 3 {
            site.Metadata.NextRetryAt = time.Now().Add(24 * time.Hour)
        }
    default:
        // 其他状态：按通用错误重试
        site.Metadata.NextRetryAt = d.calculateNextRetry(post.Status)
    }

    return d.cfgManager.SaveSite(site)
}

func (d *Daemon) tryDeployIfNew(site *config.SiteConfig, data *fetcher.CertData) error {
    newCert, err := parseCertificateFromPEM(data.Cert)
    if err != nil {
        d.logger.Error("parse new certificate failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    currentExpiry := site.Metadata.CertExpiresAt
    // 允许通过配置覆盖最小“改进”天数，默认 15 天
    minImproveDays := site.Schedule.MinImproveDays
    if minImproveDays <= 0 { minImproveDays = 15 }
    if !currentExpiry.IsZero() && newCert.NotAfter.Sub(currentExpiry) < (time.Duration(minImproveDays)*24*time.Hour) {
        d.logger.Info("skipping deploy: not improved enough",
            "current_expiry", currentExpiry, "new_expiry", newCert.NotAfter)
        return d.cfgManager.SaveSite(site)
    }
    // 验证域名覆盖
    dv := validator.NewDomainValidator(site.Domains, site.Validation.IgnoreDomainMismatch)
    if err := dv.ValidateDomainCoverage(newCert); err != nil {
        d.logger.Error("domain validation failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    // 验证本地私钥与 CA
    v := validator.New("")
    keyBytes, kerr := os.ReadFile(site.Paths.PrivateKey)
    if kerr != nil {
        d.logger.Error("read local key failed", "error", kerr)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    if err := v.ValidateKey(string(keyBytes)); err != nil {
        d.logger.Error("private key validation failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    if data.IntermediateCert != "" {
        if err := v.ValidateCA(data.IntermediateCert); err != nil {
            d.logger.Error("CA validation failed", "error", err)
            site.Metadata.NextRetryAt = d.calculateNextRetry("error")
            return d.cfgManager.SaveSite(site)
        }
    }
    // 备份
    if site.Backup.Enabled {
        bm := backup.NewManager(d.cfgManager.GetBackupDir(), site.Backup.KeepVersions)
        _, _ = bm.Backup(site.SiteName, site.Paths.Certificate, site.Paths.PrivateKey, &backup.CertInfo{
            Subject: newCert.Subject.CommonName,
            Serial:  fmt.Sprintf("%X", newCert.SerialNumber),
            NotBefore: newCert.NotBefore,
            NotAfter:  newCert.NotAfter,
        })
    }
    // 部署
    nginx := deployer.NewNginxDeployer(site.Paths.Certificate, site.Paths.PrivateKey, site.Reload.TestCommand, site.Reload.ReloadCommand)
    if err := nginx.Deploy(data.Cert, data.IntermediateCert, string(keyBytes)); err != nil {
        d.logger.Error("deployment failed", "error", err)
        site.Metadata.NextRetryAt = d.calculateNextRetry("error")
        return d.cfgManager.SaveSite(site)
    }
    site.Metadata.CertExpiresAt = newCert.NotAfter
    site.Metadata.CertSerial = fmt.Sprintf("%X", newCert.SerialNumber)
    site.Metadata.LastDeployAt = time.Now()
    site.Metadata.LastCheckAt = time.Now()
    site.Metadata.NextRetryAt = time.Time{}
    site.Metadata.IssueRetryCount = 0
    d.logger.Info("certificate deployed by daemon", "site", site.SiteName, "expires_at", newCert.NotAfter)
    return d.cfgManager.SaveSite(site)
}

// calculateNextRetry 计算下次重试时间
func (d *Daemon) calculateNextRetry(status string) time.Time {
	now := time.Now()

	// 快速重试状态
	fastRetryStates := map[string]bool{
		"unpaid":     true,
		"pending":    true,
		"processing": true,
		"approving":  true,
	}

	if fastRetryStates[status] {
		// 30分钟后重试
		return now.Add(30 * time.Minute)
	}

	// 其他状态:6小时后重试
	return now.Add(6 * time.Hour)
}

// CalculateNextCheck 计算下次检查时间(智能调度)
func CalculateNextCheck(sites []*config.SiteConfig) time.Duration {
	minDays := 999

	for _, site := range sites {
		if !site.Enabled {
			continue
		}

		days := site.DaysUntilExpiry()
		if days < minDays {
			minDays = days
		}
	}

	// 根据最近的到期时间调整检查间隔
	// > 30 天:12 小时
	// 15-30 天:6 小时
	// 7-15 天:2 小时
	// < 7 天:30 分钟

	if minDays > 30 {
		return 12 * time.Hour
	} else if minDays > 15 {
		return 6 * time.Hour
	} else if minDays > 7 {
		return 2 * time.Hour
	} else {
		return 30 * time.Minute
	}
}

// parseCertificateFromPEM 解析 PEM 字符串为 X.509 证书
func parseCertificateFromPEM(pemStr string) (*x509.Certificate, error) {
    block, _ := pem.Decode([]byte(pemStr))
    if block == nil || block.Type != "CERTIFICATE" {
        return nil, fmt.Errorf("invalid certificate PEM")
    }
    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        return nil, err
    }
    return cert, nil
}

// safeJoinWebroot 将 DCV 指定的相对路径安全地拼接到 webroot 下：
// - 仅允许 .well-known 前缀
// - 拒绝路径穿越，确保最终路径仍处于 webroot 中
func safeJoinWebroot(webroot, rel string) (string, error) {
    // 统一清理，防止 // 与 ../ 等
    cleanRel := filepath.Clean("/" + rel)
    if !strings.HasPrefix(cleanRel, "/.well-known/") {
        return "", fmt.Errorf("invalid dcv path (must under .well-known): %s", rel)
    }
    target := filepath.Join(webroot, cleanRel)
    // 归一化后再次检查，防止逃逸
    if !strings.HasPrefix(target, filepath.Clean(webroot)+string(os.PathSeparator)) {
        return "", fmt.Errorf("dcv path escapes webroot: %s", rel)
    }
    return target, nil
}

// generateCSR 生成本地私钥与 CSR（封装）
func (d *Daemon) generateCSR(commonName string, keyCfg config.KeyConfig, csrCfg config.CSRConfig) (string, string, string, error) {
    // 默认：RSA 2048
    opt := csr.KeyOptions{Type: keyCfg.Type, Size: keyCfg.Size, Curve: keyCfg.Curve}
    if opt.Type == "" { opt.Type = "rsa" }
    if opt.Type == "rsa" && opt.Size == 0 { opt.Size = 2048 }
    if opt.Type == "ecdsa" && opt.Curve == "" { opt.Curve = "prime256v1" }
    subj := csr.CSROptions{
        CommonName: commonName,
        Organization: csrCfg.Organization,
        Country: csrCfg.Country,
        State: csrCfg.State,
        Locality: csrCfg.Locality,
        Email: csrCfg.Email,
    }
    return csr.GenerateKeyAndCSR(opt, subj)
}
